#!/usr/bin/env python
from PIL import Image, ImageChops, ImageOps, ImageFont, ImageDraw
import argparse
from itertools import zip_longest
import os
import numpy as np
import pandas as pd
import re
import glob

''' Todo list
Assembly orbital graph according to orbital level
convert and trim each graph 
and equal zoom option 
'''
parser = argparse.ArgumentParser(
    description='Compose multiple image to a table of images',
    formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-c', dest='ncol', type=int, default=1,
                    help='set the number of columns')
parser.add_argument('-s', dest='sep', default='',
                    help='Define separator to separate filename into row label and column label\n')
parser.add_argument('-w', dest='width', default='2c',
                    help='set image width, if no unit is given the unit will be pixel.\n'
                    'The available unist are mm, cm, inch, c\n'
                    'c for column so 1c and 2c correspond to one and two columns figure\n'
                    'The default is 2c(180mm) and the pixel will be calculated by dpi')
parser.add_argument('-d', dest='dpi', type=int,
                    help='set the output dpi, default = 300', default=300)
parser.add_argument('-o', dest='output', type=str, default="",
                    help='set the output file name')
parser.add_argument('-l', dest='label', type=str, default='rc',
                    help='set the text label for table, default = rc\n'
                    'r, show row label\n'
                    'c, show column label\n'
                    'u, show label for each uint cell')
parser.add_argument('-t', dest='transform', type=str, default='',
                    help='Transform current table by vairous methods\n'
                    't - transpose current table.\n'
                    'r:3,1,2 or r:rowlabel2,rowlabel1 - adjust row order accordingly\n'
                    'c:3,2,1 or c:lumo,homo - adjust column order accordingly\n'
                    's:c1,c2 or s:r0,r3 - swap two columns or two rows respectively\n'
                    's:aaa,bbb - swap two columns or two rows or two elements if their namas are aaa,bbb\n'
                    's:r5c1,r2c3 or s:fname1,fname2 - switch two elements in the table\n'
                    'Multiple transformations could be separated by ;')
parser.add_argument('-T', dest='transimage', type=str, default='',
                    help='Transform image, the fomats are ele1,ele2:ops1;ele3:ops2\n'
                    'the eles are rN, cN, rNcN, where N is the row or col idx\n'
                    'starts from 0,0 in the upper left corner\n'
                    'the ops are fv for flip vertical\n'
                    'fh for flip horizontal, rN for rotate N degrees')
parser.add_argument('--line', type=str, default='t,0.5',
                    help='set the line profile, default is t,0.5.\n'
                    'which mean three line if column label is present or two line if not.\n'
                    'c for all column line, and r for all row line, o for the border,\n'
                    'x for inner grid, Z for up and down edge, N for left and right edge\n'
                    '0.5 is the width of line (in pt)')
parser.add_argument('--eq', type=str, default='f',
                    help='set table profile.\n'
                    'f, all rows and columns may have different height and width\n'
                    'r, all rows have equal height\n'
                    'c, all cols have equal width\n'
                    'rc, all cols have equal width and all rows have equal height\n')
parser.add_argument('--iborder', type=str, default="2mm",
                    help='The border width is the white space around each image\n'
                    'one value for 4 sides and 2 value separated by comma for widht and height\n'
                    'no unit for pixel and the unit can be mm cm inch ')
parser.add_argument('--lfont', type=str, default='',
                    help='set the label font filename (not font name) default is arial')
parser.add_argument('--lsize', type=float, default=10,
                    help='set the label font size in pt, default is 10 pt.\n'
                    'The actual font size in pixel is detemined by dpi (-d option)\n')
parser.add_argument('--lborder', type=str, default="1a,1a",
                    help='set the label border, one value for 4 sides\n'
                    'and two value separated by comma for width and height, respectively\n'
                    'no unit for pixel and the unit can be mm cm inch and a for alphabet.\n'
                    'default is 2a,0\n')
parser.add_argument('--lsub', type=str, default='',
                    help='set the label substitution, the format is\n'
                    'origin_str1:new_str1;orgin_str2:new_str2')
parser.add_argument('--lpos', type=str, default='cc',
                    help='use 2 letter to set the label position, default is cc\n'
                    'first letter is one of vertical (u,c,d) and second is one of horizontal (l,c,r)\n'
                    'where l for left, r for right, u for up\n'
                    'd for down, c for horizon or vertical center\n'
                    'use r:lc,c:cu,u:cu to set row, col, unitcell separaterly')
parser.add_argument('--rlabel', type=str, default='',
                    help='set the row label, seperate by comma, use \\n for multiline')
parser.add_argument('--clabel', type=str, default='',
                    help='set the column label, seperate by comma, use \\n for multiline')
parser.add_argument('--ulabel', type=str, default='',
                    help='set the unit cell label, seperate by comma\n'
                    'the order is:\n'
                    '1,2,3\n'
                    '4,5,6\n')
parser.add_argument('inputfile', nargs='+', help='the input figures')
args = parser.parse_args()


class FigArrange:
    def __init__(self, args):
        self.figure_files = []
        for i in args.inputfile:
            self.figure_files += glob.glob(i)
        self.sep = args.sep
        self.eq = args.eq
        self.label = args.label
        self.iborder = args.iborder  # image border, render by self.border_parser()
        self.lborder = args.lborder  # label border, rendered by self.border_parser()
        self.rlabel = args.rlabel
        self.clabel = args.clabel
        self.ulabel = args.ulabel
        self.lsub = args.lsub
        self.ncol = args.ncol
        self.dpi = args.dpi
        self.line = args.line
        self._parser_lpos(args.lpos)
        self.font = args.lfont
        self.font_size = int(np.rint(args.lsize/72*self.dpi))
        self.width = self._convert2pixel(args.width)
        self.height = int(np.rint(297/25.4*float(self.dpi)))
        self._load_font()  # convet self.font to PIL font object
        self.trans_str = args.transform
        self.transimage = args.transimage
        self.flnm_matrix_gen()
        self.gen_label()  # auto generate col and row labels
        self.transform_table()  # transform the dataframe by user's input via -t
        if self.transimage:
            self.transform_image()  # transform image by user's -T option
        self.border_parser()
        self.label_replace()  # replace label string by user's input via -lreplace
        self.label_size()  # calculate
        self.make_cell()
        self.composer()
        self.show_info()

    def _parser_lpos(self, lpos_str):
        self.lpos = {'r': 'cc', 'c': 'cc', 'u': 'cc'}
        if ',' in lpos_str or ':' in lpos_str:
            for i in lpos_str.split(','):
                self.lpos[i.split(':')[0]] = i.split(':')[1]
        elif len(lpos_str) == 2:
            for k in self.lpos.keys():
                self.lpos[k] = lpos_str
        else:
            print('Error!!! Wrong format of lpos')

    def _load_font(self):
        '''1 try to load user defined font.
        Then try to load font in font_family one by one'''
        font_family = ['arial', 'DejaVuSansCondensed','DejaVuSans']
        if self.font:
            try:
                font = ImageFont.truetype(self.font, self.font_size)
            except OSError:
                print("font file {:s} not found in the system or in the current directory".format(
                    self.font))
        else:
            found = 0
            for f in font_family:
                try:
                    font = ImageFont.truetype(f, self.font_size)
                except OSError:
                    pass
                else:
                    print("font file {:s} found and used".format(f))
                    found = 1
                    break
            if found == 0:
                font = ImageFont.load_default()
                print("font file not found, use the default font")
        self.font = font

    def _convert2pixel(self, input, fontwidth=0):
        '''convert all unit to pixel, using self.width and self.dpi
        generate self.wid.h in pixel'''
        if input.endswith('mm'):
            out = float(input.strip('mm'))/25.4*float(self.dpi)
        elif input.endswith('cm'):
            out = float(input.strip('cm'))/2.54*float(self.dpi)
        elif input.endswith('in') or input.endswith('inch'):
            out = float(input.strip('inch'))*float(self.dpi)
        elif input.endswith('c'):
            out = float(input.strip('c'))*90/25.4*float(self.dpi)
        elif input.endswith('a'):
            out = float(input.strip('a'))*fontwidth
        elif input.isdigit():
            out = float(input)
        return int(np.rint(out))

    def _trim(self, im, tol=10):
        '''Trim the border from image'''
        bg = Image.new(im.mode, im.size, im.getpixel((0, 0)))
        diff = ImageChops.difference(im, bg)
        diff = ImageChops.add(diff, diff, 2.0, tol*-1)
        bbox = diff.getbbox()
        if bbox:
            return im.crop(bbox)

    def _transpose(self):
        self.tdf = self.tdf.T

    def _sort_row(self, row_order):
        reorder_list = row_order.strip(':').split(',')
        rows = self.tdf.index
        if all([i.isdigit() for i in reorder_list]):
            new_rows = [int(i)-1 for i in reorder_list]
        else:
            new_rows = []
            for i in reorder_list:
                new_rows += [j for j,k in enumerate(rows) if k == i]
        if None in new_rows:
            print('Error!!! Some elements not found in column label')
            os._exit(0)
        self.tdf = self.tdf.iloc[new_rows, :]

    def _sort_col(self, col_order):
        reorder_list = col_order.strip(':').split(',')
        cols = self.tdf.columns
        if all([i.isdigit() for i in reorder_list]):
            new_cols = [int(i)-1 for i in reorder_list]
        else:
            new_cols = []
            for i in reorder_list:
                new_cols += [j for j,k in enumerate(cols) if k == i]
        if None in new_cols:
            print('Error!!! Some elements not found in column label')
            os._exit(0)
        self.tdf = self.tdf.iloc[:,new_cols]

    def _switch_elem(self, elem_pair):
        cols = list(self.tdf.columns)
        rows = list(self.tdf.index)
        ele1 = elem_pair.strip(':').split(',')[0]
        ele2 = elem_pair.strip(':').split(',')[1]
        if re.match("^r\d+$", ele1) and re.match("^r\d+$", ele2):
            a, b = int(ele1.strip('r')), int(ele2.strip('r'))
            rows[a], rows[b] = rows[b], rows[a]
            self.tdf = self.tdf.reindex(rows)
        elif re.match("^c\d+$", ele1) and re.match("^c\d+$", ele2):
            a, b = int(ele1.strip('c')), int(ele2.strip('c'))
            cols[a], cols[b] = cols[b], cols[a]
            self.tdf = self.tdf[cols]
        elif re.match("^r\d+c\d+$", ele1) and re.match("^r\d+c\d+$", ele2):
            e1, e2 = [int(i) for i in ele1.strip('r').split('c')]
            v1 = self.tdf.iloc[e1, e2]
            e3, e4 = [int(i) for i in ele2.strip('r').split('c')]
            v2 = self.tdf.iloc[e3, e4]
            self.tdf.iloc[e3, e4], self.tdf.iloc[e1, e2] = v1, v2
        elif ele1 in self.tdf.values and ele2 in self.tdf.values:
            r1, c1 = self.tdf[self.tdf == ele1].stack().index.tolist()[0]
            r2, c2 = self.tdf[self.tdf == ele2].stack().index.tolist()[0]
            self.tdf.loc[r1, c1] = ele2
            self.tdf.loc[r2, c2] = ele1
        elif ele1 in cols and ele2 in cols:
            a, b = cols.index(ele1), cols.index(ele2)
            cols[a], cols[b] = cols[b], cols[a]
            self.tdf = self.tdf[cols]
        elif ele1 in rows and ele2 in rows:
            a, b = rows.index(ele1), rows.index(ele2)
            rows[a], rows[b] = rows[b], rows[a]
            self.tdf = self.tdf.reindex(rows)

    def _findstem(self, arr):
        # Take first word from array
        # as reference
        s = arr[0]
        l = len(s)
        res = ""
        for i in range(l):
            for j in range(i + 1, l + 1):
                # generating all possible substrings
                # of our reference string arr[0] i.e s
                stem = s[i:j]
                for ss in [i for i in arr if i is not None]:
                    # Check if the generated stem is
                    # common to all words
                    if stem not in ss:
                        stem = ""
                        break
                # If current substring is present in
                # all strings and its length is greater
                # than current result
                if len(res) < len(stem):
                    res = stem
        return res

    def _trans_image(self, image_obj, trans_str):
        if trans_str.lower() == 'fv':
            transed = image_obj.transpose(Image.FLIP_TOP_BOTTOM)
        if trans_str.lower() == 'fh':
            transed = image_obj.transpose(Image.FLIP_LEFT_RIGHT)
        if trans_str.lower().startswith('r'):
            degree = trans_str.strip('r')
            rotated = image_obj.rotate(
                float(degree), resample=Image.BICUBIC, expand=True, fillcolor='white')
            transed = self._trim(rotated)
        return transed

    def transform_image(self):
        for o in self.transimage.split(';'):
            ele_list, ops = o.split(':')
            for ele in ele_list.split(','):
                trans_dict = {}
                if re.match("^r\d+$", ele):
                    idx = int(ele.strip('r'))
                    row_dict = {i: self.n2obj[i]
                                for i in self.tdf_list[idx]}
                    for k, v in row_dict.items():
                        trans_dict[k] = self._trans_image(v, ops)
                elif re.match("^c\d+$", ele):
                    idx = int(ele.strip('c'))
                    col_dict = {i: self.n2obj[i]
                                for i in self.tdf_listT[idx]}
                    for k, v in col_dict.items():
                        trans_dict[k] = self._trans_image(v, ops)
                elif re.match("^r\d+c\d+$", ele):
                    e1, e2 = [int(i) for i in ele.strip('r').split('c')]
                    k = self.tdf.iloc[e1, e2]
                    v = self.n2obj[k]
                    trans_dict[k] = self._trans_image(v, ops)
                elif ele in self.tdf.values:
                    k = ele
                    v = self.n2obj[k]
                    trans_dict[k] = self._trans_image(v, ops)
                self.n2obj.update(trans_dict)
           # except ValueError:
           #     print(
           #         "Warning ignore invalid transform command {:s}".format(o))

    def flnm_matrix_gen(self):
        '''generate initial filename matrix base on self.ncol
        build self.n2obj dict which is map form basename in table
        to image file object. Build self.flist which is a 2D list
        contain all the basename'''
        flnm = [os.path.basename(i) for i in self.figure_files]
        basename = [os.path.splitext(i)[0] for i in flnm]
        image_obj = [self._trim(Image.open(i)) for i in self.figure_files]
        # image_obj = [Image.open(i) for i in self.figure_files]
        self.n2obj = dict(zip(basename, image_obj))
        flist = []
        if self.ncol:
            for i, f in enumerate(basename):
                if i % self.ncol == 0:
                    rlist = []
                rlist.append(f)
                if (i+1) % self.ncol == 0 or i+1 == len(basename):
                    flist.append(rlist)
        self.flist = flist

    def gen_label(self):
        '''set pandas dataframe index for 2D list by either
        use separator(self.sep) in basename or by find max
        common string in rows and columns in basename. Generate
        self.df and self.tdf properties, and the latter is for transformation'''
        ridx = []
        cidx = []
        tflist = [i for i in zip_longest(*self.flist)]
        if self.sep:
            for r in self.flist:
                head = [i.split(self.sep)[0] for i in r]
                tail = [i.split(self.sep)[1] for i in r]
                if len(set(head)) == 1:
                    ridx.append(list(set(head))[0])
                elif len(set(tail)) == 1:
                    ridx.append(list(set(tail))[0])
                elif len(set(head)) > len(set(tail)):
                    ridx.append(''.join(list(set(tail))))
                else:
                    ridx.append(''.join(list(set(head))))
            for c in tflist:
                head = [i.split(self.sep)[0] for i in c if i is not None]
                tail = [i.split(self.sep)[1] for i in c if i is not None]
                if len(set(head)) == 1:
                    cidx.append(list(set(head))[0])
                elif len(set(tail)) == 1:
                    cidx.append(list(set(tail))[0])
                elif len(set(head)) >= len(set(tail)):
                    cidx.append(''.join(list(set(tail))))
                else:
                    cidx.append(''.join(list(set(head))))
        else:
            for i, r in enumerate(self.flist):
                stem = self._findstem(r)
                if stem:
                    ridx.append(stem)
                else:
                    ridx.append(str(i))
            for i, c in enumerate(tflist):
                stem = self._findstem(c)
                if stem:
                    cidx.append(self._findstem(c))
                else:
                    cidx.append(str(i))
            coms = self._findstem(cidx+ridx)
            r, c = [], []
            for i, x in enumerate(cidx):
                y = x.replace(coms, '')
                if y:
                    c.append(y)
                else:
                    c.append(str(i))
            for i, x in enumerate(ridx):
                y = x.replace(coms, '')
                if y:
                    r.append(y)
                else:
                    r.append(str(i))
            cidx, ridx = c, r
        self.cidx, self.ridx = cidx, ridx
        df = pd.DataFrame(self.flist)
        df.columns = self.cidx
        df.index = self.ridx
        self.df = df
        self.tdf = df

    def transform_table(self):
        '''preform various transformation base on self.trans_str
        generate self.tdf properties. Set user defined label after transform
        also '''
        self.trans_dict = {'t': self._transpose, 'r': self._sort_row,
                           'c': self._sort_col, 's': self._switch_elem}
        if self.trans_str:
            for ts in self.trans_str.split(';'):
                if ts[0] == "t":
                    self.trans_dict["t"]()
                else:                   
                    self.trans_dict[ts[0]](ts[1:])
        if self.rlabel:
            self.tdf.index = [i.encode('utf-8').decode('unicode_escape') 
                              for i in self.rlabel.split(',')]
        if self.clabel:
            self.tdf.columns = [i.encode('utf-8').decode('unicode_escape') 
                                for i in self.clabel.split(',')]
        self.tdf_list = [[i for i in row if i is not None]
                         for row in self.tdf.values.tolist()]
        self.tdf_listT = [[i for i in row if i is not None]
                          for row in self.tdf.T.values.tolist()]

    def label_replace(self):
        '''replace unit label by user specified rules
        and generate '''
        self.n2label = {}
        for k, v in self.n2obj.items():
            self.n2label[k] = k
        if self.lsub:
            for i in self.lsub.split(';'):
                old, new = i.split(':')
                for k, v in self.n2label.items():
                    self.n2label[k] = v.replace(old, new)
        if self.ulabel:
            nlist = sum(self.tdf_list, [])
            llist = [i.encode('utf-8').decode('unicode_escape') for i in self.ulabel.split(',')]
            self.n2label = dict(zip(nlist, llist))

    def label_size(self):
        '''calculate all the label size(in pixel)'''
        im = Image.new(mode='RGB', size=(2000, 2000))
        draw = ImageDraw.Draw(im)
        unit_lsize = {}
        row_lsize = {}
        column_lsize = {}
        for row in self.tdf_list:
            hs=[]
            for i in row:
                bbox = draw.textbbox((0, 0), self.n2label[i], self.font)
                hs.append(bbox[3] - bbox[1]+self.lborder[1]+self.lborder[3])
            h=max(hs)
            for u in row:
                bbox = draw.textbbox((0, 0), self.n2label[u], self.font)
                unit_lsize[u] = (bbox[2] - bbox[0], h)
        for k, v in self.n2label.items():
            bbox = draw.textbbox((0, 0), v, self.font)
            unit_lsize[k] = (bbox[2] - bbox[0], bbox[3] - bbox[1])
        for k in list(self.tdf.index):
            bbox = draw.textbbox((0, 0), k, self.font)
            row_lsize[k] = (bbox[2] - bbox[0], bbox[3] - bbox[1])
        for k in list(self.tdf.columns):
            bbox = draw.textbbox((0, 0), k, self.font)
            column_lsize[k] = (bbox[2] - bbox[0], bbox[3] - bbox[1])
        self.rlabel_w = max([i[0] for i in row_lsize.values()]
                            ) + self.lborder[0] + self.lborder[2]
        self.clabel_h = max([i[1] for i in column_lsize.values()]
                            ) + self.lborder[1] + self.lborder[3]
        self.ulabel_s = unit_lsize

    def _fill_cell(self, source, target):
        '''source and target are tuples of width and height
        will return the resize (2-tuple:width,height) and
        the margin (2-tuple: delta_w, delta_h) and
        the target size (2-tuple: width,height) and
        the blank area introduced by padding'''
        new_h = np.rint(target[0]/source[0]*source[1])
        new_w = np.rint(target[1]/source[1]*source[0])
        if new_h > target[1]:
            new_h = np.rint(target[1])
        if new_w > target[0]:
            new_w = np.rint(target[0])
        if new_w/source[0] > 1.5:
            print('Warning! Image expand 150% ')
        resize = (int(new_w), int(new_h))
        dw = int(np.rint(target[0] - new_w))
        dh = int(np.rint(target[1] - new_h))
        # blank_area = np.rint(target[0] * target[1] - new_w*new_h)
        tw, th = [int(np.rint(i)) for i in target]
        return resize, (dw, dh), (tw, th)

    def make_cell(self):
        '''detemine resize, margin size, position of each image (unit cell)
        four dict will generated and the key is basename, the values are
        generated by self._fill method and are 4-tuple of resize,margin,target_size,blank_area
        The self.eq value choose the dict from above four dict and generate
        self.n2cell properties
         '''
        n2s = {}
        all2eq_dict = {}  # adjustment to make all cell eq
        col2eq_dict = {}  # adjustment to make all col eq width
        row2eq_dict = {}  # adjustment to make all row eq height
        free2eq_dict = {}  # free adjustment that make each row same height and each col same width
        for k, v in self.n2obj.items():
            n2s[k] = v.size
        all_h = np.quantile([i[1] for i in n2s.values()], 0.75)
        all_w = np.quantile([i[0] for i in n2s.values()], 0.75)
        row_h = []
        col_w = []
        for r in self.tdf_list:
            row_h.append(np.mean([self.n2obj[i].size[1] for i in r]))
        for c in self.tdf_listT:
            col_w.append(np.mean([self.n2obj[i].size[0] for i in c]))
        for i, r in enumerate(self.tdf_list):
            for j, c in enumerate(r):
                all2eq_dict[k] = (all_w, all_h)
                col2eq_dict[c] = (all_w, row_h[i])
                row2eq_dict[c] = (col_w[j], all_h)
                free2eq_dict[c] = (col_w[j], row_h[i])
        p2d = {'r': row2eq_dict, 'c': col2eq_dict,
               'rc': all2eq_dict, 'f': free2eq_dict}
        self.n2cell = p2d[self.eq]

    def border_parser(self):
        '''render user input border to a 2-tuple with pixel as unit'''
        ib = []
        for i in self.iborder.split(','):
            ib.append(self._convert2pixel(i))
        if len(ib) == 1:
            iborder = [ib[0], ib[0], ib[0], ib[0]]
        if len(ib) == 2:
            iborder = [ib[0], ib[1], ib[0], ib[1]]
        im = Image.new(mode='RGB', size=(2000, 2000))
        draw = ImageDraw.Draw(im)
        self.iborder = iborder
        lsize = draw.textlength('N', self.font)
        lb = []
        for i in self.lborder.split(','):
            lb.append(self._convert2pixel(i, fontwidth=lsize))
        if len(lb) == 1:
            lborder = [lb[0], lb[0], lb[0], lb[0]]
        if len(lb) == 2:
            lborder = [lb[0], lb[1], lb[0], lb[1]]
        self.lborder = lborder

    def composer(self):
        '''If the cavas width is set, then the total width
        is the canvas width minus the row label width. And the size of image
        if resized to fit the total width. The unit cell label and column label
        will not affect the composer'''
        if self.width:
            trgt_iw = self.width - self.iborder[0]*self.ncol*2
            if 'r' in self.label:
                trgt_iw = trgt_iw - self.rlabel_w
            crnt_iw = sum([self.n2cell[i][0] for i in self.tdf_list[0]])
            ratio = trgt_iw/crnt_iw
        else:
            ratio = 1
        n2cell_resize = {}
        for k, v in self.n2cell.items():
            new_w = int(np.rint(v[0] * ratio))
            new_h = int(np.rint(v[1] * ratio))
            n2cell_resize[k] = self._fill_cell(
                self.n2obj[k].size, (new_w, new_h))
        self.n2cell = n2cell_resize
        rlabel_h = []
        for u in self.tdf_listT[0]:
            cell_h = self.n2cell[u][2][1] + self.iborder[1] + self.iborder[3]
            if 'u' in self.label:
                cell_h = cell_h + self.ulabel_s[u][1]
            rlabel_h.append(cell_h)
        clabel_w = []
        for u in self.tdf_list[0]:
            clabel_w.append(self.n2cell[u][2][0] +
                            self.iborder[0] + self.iborder[2])
        self.clabel_w = clabel_w
        self.rlabel_h = rlabel_h
        self.ratio = ratio

    def show_info(self):
        iw = sum([self.n2cell[i][2][0] for i in self.tdf_list[0]])
        ih = sum([self.n2cell[i][2][1] for i in self.tdf_listT[0]])
        iw = iw + self.iborder[0]*self.ncol*2
        ih = ih + self.iborder[1]*len(self.tdf_listT[0])*2
        rlblw, clblh, ulblh = 0, 0, 0
        if 'r' in self.label:
            rlblw = self.rlabel_w
            iw = iw + rlblw
        if 'c' in self.label:
            clblh = self.clabel_h
            ih = ih + self.clabel_h
        if 'u' in self.label:
            ulabel_h = []
            for r in self.tdf_list:
                ulabel_h.append(max([self.ulabel_s[i][1] for i in r]))
            ulblh = sum(ulabel_h)
            ih = ih + ulblh
        print(self.tdf.replace(self.n2label))
        print('zoom all cell by {:.3f} to fit the canvas'.format(self.ratio))
        print('Graph width is {:.1f} mm ({:d} pixel); height is {:.1f} mm ({:d} pixel)'
              .format(iw/self.dpi*25.4, iw, ih/self.dpi*25.4, ih))
        if clblh == 0:
            lblh = ulblh
        else:
            lblh = clblh
        print('label width is {:.1f} mm ({:d} pixel); height is {:.1f} mm ({:d} pixel)'
              .format(rlblw/self.dpi*25.4, rlblw, lblh/self.dpi*25.4, lblh))

    def process_unit_image(self):
        '''trim and pad all the images according to the self.eq properties
        and *_dict properties generated by self.composer()
        '''
        n2trim = {}
        n2tpad = {}
        self.n2proc = {}
        for k, v in self.n2obj.items():
            if k in self.n2cell:
                n2trim[k] = v.resize(
                    self.n2cell[k][0], resample=Image.LANCZOS)
        for k, v in n2trim.items():
            w, h = self.n2cell[k][1]
            pad = tuple([int(i) for i in [w/2, h/2, w/2, h/2]])
            n2tpad[k] = ImageOps.expand(v, border=pad, fill='white')
        if 'u' in self.label:
            addlbl = {}
            for k, v in n2tpad.items():
                w = v.size[0]
                h = self.ulabel_s[k][1]
                lbl = self._gen_label(
                    (w, h), self.n2label[k], pos=self.lpos['u'])
                addlbl[k] = self.vstack([v, lbl])
            n2tpad = addlbl
        for k, v in n2tpad.items():
            self.n2proc[k] = ImageOps.expand(
                v, border=tuple(self.iborder), fill='white')

    def _gen_label(self, bg_size, lbl_str, pos='cc'):
        im_temp = Image.new(mode='RGB', size=(2000, 2000))
        draw_temp = ImageDraw.Draw(im_temp)
        # lbl_str = lbl_str.encode('utf-8').decode('unicode_escape')
        text_bbox = draw_temp.textbbox((0, 0), lbl_str, font=self.font)
        text_size = (text_bbox[2] - text_bbox[0],text_bbox[3] - text_bbox[1])
        # print(text_size)
        # print(lbl_str)
        im = Image.new(mode='RGB', size=bg_size, color='white')
        draw = ImageDraw.Draw(im)
        up = self.lborder[1]
        down = bg_size[1] - self.lborder[3] - text_size[1]
        mid = int(np.rint(bg_size[1]/2 - text_size[1]/2))
        left = self.lborder[0]
        right = bg_size[0] - text_size[0] - self.lborder[2]
        center = int(np.rint(bg_size[0]/2-text_size[0]/2))
        pos_dict = {'ul': (left, up), 'uc': (center, up), 'ur': (right, up),
                    'cl': (left, mid), 'cc': (center, mid), 'cr': (right, mid),
                    'dl': (left, down), 'dc': (center, down), 'dr': (right, down),
                    }
        align_dict = {'l': 'left', 'c': 'center', 'r': 'right'}
        draw.text(pos_dict[pos], lbl_str, font=self.font, fill=0,
                  align=align_dict[pos[-1]])
        return(im)

    def vstack(self, image_list):
        ws = [i.size[0] for i in image_list]
        hs = [i.size[1] for i in image_list]
        w = max(ws)
        h = sum(hs)
        stacked = Image.new('RGB', (w, h), color='white')
        hp = 0
        for i, m in enumerate(image_list):
            # wp = int(np.rint((w-m.size[0])/2))
            wp = 0
            stacked.paste(m, box=(wp, hp))
            hp = hp + m.size[1]
        return stacked

    def hstack(self, image_list):
        ws = [i.size[0] for i in image_list]
        hs = [i.size[1] for i in image_list]
        w = sum(ws)
        h = max(hs)
        stacked = Image.new('RGB', (w, h), color='white')
        wp = 0
        for i, m in enumerate(image_list):
            hp = int(np.rint((h-m.size[1])/2))
            stacked.paste(m, box=(wp, hp))
            wp = wp + m.size[0]
        return stacked

    def gen_row_label(self):
        lbl = []
        for i, s in enumerate(self.tdf.index):
            w = self.rlabel_w
            h = self.rlabel_h[i]
            lbl.append(self._gen_label((w, h), s, pos=self.lpos['r']))
        return self.vstack(lbl)

    def gen_col_label(self):
        if 'r' in self.label:
            # generate upper left empty cell
            lbl = [self._gen_label((self.rlabel_w, self.clabel_h), "")]
        else:
            lbl = []
        for i, s in enumerate(self.tdf.columns):
            h = self.clabel_h
            w = self.clabel_w[i]
            lbl.append(self._gen_label((w, h), s, pos=self.lpos['c']))
        return self.hstack(lbl)

    def stack_all(self):
        '''add label to image'''
        rows = []
        for r in self.tdf_list:
            rows.append(self.hstack([self.n2proc[i] for i in r]))
        final_image = self.vstack(rows)
        if 'r' in self.label:
            final_image = self.hstack([self.gen_row_label(), final_image])
        if 'c' in self.label:
            final_image = self.vstack([self.gen_col_label(), final_image])
        self.image = final_image

    def _draw_multi_line(self, line_coords):
        draw = ImageDraw.Draw(self.image)
        for l in line_coords:
            draw.rectangle(l, fill='black')

    def add_line(self):
        if len(self.line.split(',')) == 1:
            style = self.line.split(',')[0]
            width = int(np.rint(1/72*self.dpi/2))
        elif len(self.line.split(',')) == 2:
            style, width = self.line.split(',')
            width = int(np.rint(float(width)/72*self.dpi/2))
        row_grid = [0]
        for h in self.rlabel_h:
            h = h + row_grid[-1]
            row_grid.append(h)
        col_grid = [0]
        for w in self.clabel_w:
            w = w + col_grid[-1]
            col_grid.append(w)
        if 'r' in self.label:
            col_grid = [0] + [i+self.rlabel_w for i in col_grid]
        if 'c' in self.label:
            row_grid = [0] + [i+self.clabel_h for i in row_grid]
        rl = [[(col_grid[0], i-width), (col_grid[-1], i+width)]
              for i in row_grid]
        cl = [[(i-width, row_grid[0]), (i+width, row_grid[-1])]
              for i in col_grid]
        rl[0] = [(0, row_grid[0]), (col_grid[-1], row_grid[0]+width*2)]
        rl[-1] = [(0, row_grid[-1]-width*2), (col_grid[-1], row_grid[-1])]
        cl[0] = [(col_grid[0], row_grid[0]),
                 (col_grid[0]+width*2, row_grid[-1])]
        cl[-1] = [(col_grid[-1]-width*2, row_grid[0]),
                  (col_grid[-1], row_grid[-1])]
        if 'o' in style:
            lines = [rl[0], rl[-1], cl[0], cl[-1]]
            self._draw_multi_line(lines)
        if 'c' in style:
            self._draw_multi_line(cl)
        if 'r' in style:
            self._draw_multi_line(rl)
        if 'x' in style:
            self._draw_multi_line(rl[1:-1]+cl[1:-1])
        if 't' in style:
            self._draw_multi_line([rl[0], rl[-1]])
            if 'c' in self.label:
                self._draw_multi_line([rl[1]])
        if 'N' in style:
            self._draw_multi_line([cl[0], cl[-1]])
        if 'Z' in style:
            self._draw_multi_line([rl[0], rl[-1]])

    def show_save(self, filename):
        self.image.save(filename)
        self.image.show()


f = FigArrange(args)
if args.output:
    f.process_unit_image()
    f.stack_all()
    f.add_line()
    f.show_save(args.output)
